Skip to content

Conversation

admin-coderabbit
Copy link
Owner

@admin-coderabbit admin-coderabbit commented Feb 2, 2026

This pull request was automatically created by @coderabbitai/e2e-reviewer.

Batch created pull request.

Summary by CodeRabbit

  • New Features

    • Added assignment source tracking to prevent sync cycles when external integrations modify issue assignments—assignments from the same integration source will no longer trigger redundant synchronization.
  • Tests

    • Added test coverage for assignment source creation, validation, and serialization.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbit-eval
Copy link

coderabbit-eval bot commented Feb 2, 2026

📝 Walkthrough

Walkthrough

The changes introduce AssignmentSource tracking to prevent sync cycles between Sentry and external integrations. A new immutable dataclass captures the origin (integration name, ID, timestamp) of group assignments. This information flows through assignment operations into sync decision logic, where duplicate syncs to the originating integration are prevented.

Changes

Cohort / File(s) Summary
Assignment Source Data Structure
src/sentry/integrations/services/assignment_source.py
New immutable dataclass with source_name, integration_id, and queued timestamp. Includes factory methods from_integration() and from_dict() for construction, and to_dict() for serialization.
Integration Cycle Prevention
src/sentry/integrations/mixins/issues.py
Updated should_sync() signatures in IssueBasicIntegration and IssueSyncIntegration to accept optional sync_source parameter. Added cycle-prevention logic: if sync source matches current integration, returns False. Updated sync_status_outbound() abstract method to accept optional assignment_source parameter.
Outbound Sync Task
src/sentry/integrations/tasks/sync_assignee_outbound.py
Extended function signature to accept optional assignment_source_dict parameter. Parses dict into AssignmentSource and passes it to installation.should_sync() and installation.sync_assignee_outbound() for cycle prevention.
Sync Utilities
src/sentry/integrations/utils/sync.py
Updated sync_group_assignee_outbound() to accept optional assignment_source parameter. Threads source through to task payload via assignment_source.to_dict(). Uses AssignmentSource.from_integration() when recording inbound sync operations.
Group Assignee Model
src/sentry/models/groupassignee.py
Updated GroupAssigneeManager.assign() and GroupAssigneeManager.deassign() to accept optional assignment_source parameter. Propagates source to outbound sync calls.
Test Coverage
tests/sentry/integrations/services/test_assignment_source.py, tests/sentry/models/test_groupassignee.py
New test module for AssignmentSource with tests for from_dict(), to_dict(), and error handling. Updated existing tests to verify cycle prevention (outbound sync skipped when source matches integration).

Sequence Diagram

sequenceDiagram
    participant ExtSys as External System
    participant Sentry as Sentry (Inbound)
    participant GAM as GroupAssignee Mgr
    participant Sync as Sync Utilities
    participant Task as Task Queue
    participant ExtInteg as External Integration

    ExtSys->>Sentry: Sync assignee change
    Sentry->>GAM: assign(group, user, assignment_source=ext_integration)
    GAM->>Sync: sync_group_assignee_outbound(group, user, assignment_source)
    Sync->>Task: Queue task with assignment_source_dict
    
    Task->>ExtInteg: should_sync("assignee", sync_source=assignment_source)
    alt Cycle Prevention Triggered
        ExtInteg-->>Task: False (source matches this integration)
        Task-->>Sync: Skip outbound sync
    else Normal Flow
        ExtInteg-->>Task: True (different integration)
        Task->>ExtInteg: sync_assignee_outbound(external_issue, user, assignment_source)
        ExtInteg-->>ExtSys: Sync confirmation
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 A cycle once spun 'round and 'round,
Now AssignmentSource keeps us bound,
No sync loops shall return to sender,
Each integration, a separate gender,
With queued timestamps in the queue! 🎉

🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'Automated Test: ecosystem-sync-integration-after' does not clearly describe the main changes in the pull request, which involve adding cycle-prevention logic for assignment syncing and introducing an AssignmentSource data structure. Replace with a descriptive title that captures the primary change, such as 'Add cycle-prevention logic for assignment source syncing' or 'Introduce AssignmentSource to prevent integration sync cycles'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbit eval help to get the list of available commands and usage tips.

@coderabbit-eval coderabbit-eval bot closed this Feb 2, 2026
Copy link

@coderabbit-eval coderabbit-eval bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/sentry/integrations/services/assignment_source.py`:
- Line 18: The queued default is set by calling timezone.now() at
class-definition time, causing all AssignmentSource instances to share a single
timestamp; change the queued field to use a default factory so it is evaluated
per-instance (e.g., use dataclasses.field(default_factory=timezone.now) or the
equivalent factory pattern in the class) and import field if necessary; update
the AssignmentSource definition to reference field(default_factory=timezone.now)
for the queued attribute.
🧹 Nitpick comments (2)
tests/sentry/integrations/services/test_assignment_source.py (2)

8-8: Minor: Test method name is misleading.

The method tests an empty dict, not an empty array. Consider renaming to test_from_dict_empty_dict for clarity.


13-13: Typo in test method name: "inalid" → "invalid".

✏️ Proposed fix
-    def test_from_dict_inalid_data(self):
+    def test_from_dict_invalid_data(self):

class AssignmentSource:
source_name: str
integration_id: int
queued: datetime = timezone.now()

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

timezone.now() is evaluated once at class definition time, not per instance.

The default value timezone.now() is called when the module loads, so every AssignmentSource instance without an explicit queued argument will share the same stale timestamp. Use field(default_factory=...) instead.

🐛 Proposed fix
-from dataclasses import asdict, dataclass
+from dataclasses import asdict, dataclass, field
 from datetime import datetime
 from typing import TYPE_CHECKING, Any
 
 from django.utils import timezone
 
 if TYPE_CHECKING:
     from sentry.integrations.models import Integration
     from sentry.integrations.services.integration import RpcIntegration
 
 
 `@dataclass`(frozen=True)
 class AssignmentSource:
     source_name: str
     integration_id: int
-    queued: datetime = timezone.now()
+    queued: datetime = field(default_factory=timezone.now)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
queued: datetime = timezone.now()
from dataclasses import asdict, dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING, Any
from django.utils import timezone
if TYPE_CHECKING:
from sentry.integrations.models import Integration
from sentry.integrations.services.integration import RpcIntegration
`@dataclass`(frozen=True)
class AssignmentSource:
source_name: str
integration_id: int
queued: datetime = field(default_factory=timezone.now)
🧰 Tools
🪛 Ruff (0.14.14)

[warning] 18-18: Do not perform function call timezone.now in dataclass defaults

(RUF009)

🤖 Prompt for AI Agents
In `@src/sentry/integrations/services/assignment_source.py` at line 18, The queued
default is set by calling timezone.now() at class-definition time, causing all
AssignmentSource instances to share a single timestamp; change the queued field
to use a default factory so it is evaluated per-instance (e.g., use
dataclasses.field(default_factory=timezone.now) or the equivalent factory
pattern in the class) and import field if necessary; update the AssignmentSource
definition to reference field(default_factory=timezone.now) for the queued
attribute.

Sign in to join this conversation on GitHub.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

1 participant